contents

컴퓨터는 0.1이나 3.14와 같은 대부분의 십진수를 부동 소수점 산술이라는 근사치 계산법을 사용하여 계산합니다. 컴퓨터의 CPU는 이진수(2진법)를 기반으로 작동하기 때문에, 대부분의 10진법 분수를 완벽하게 표현할 수 없습니다.


핵심 문제: 2진법 vs 10진법

컴퓨터는 모든 것을 비트(0과 1)로 저장합니다.

문제는 10진법에서는 흔한 많은 분수들이 2진법에서는 완벽하게 표현될 수 없다는 것입니다. 가장 유명한 예가 0.1입니다.

컴퓨터는 저장 공간이 유한하기 때문에 이 숫자를 특정 지점에서 잘라내야 하고, 결국 0.1의 _근사치_를 저장하게 됩니다. 이것이 모든 부동 소수점 "부정확성"의 근본 원인입니다.


해결책: 부동 소수점 (과학적 표기법)

이러한 숫자들을 다루기 위해 컴퓨터는 본질적으로 2진수 과학적 표기법인 시스템을 사용합니다. 이 표준을 IEEE 754라고 합니다.

비유 (과학적 표기법):

10진법에서 우리는 1,234,500,000과 같은 거대한 숫자를 $1.2345 \times 10^9$로 씁니다.

여기에는 세 부분이 있습니다.

컴퓨터의 방식 (IEEE 754):

64비트 "double" (십진수를 나타내는 일반적인 타입)은 숫자를 정확히 같은 방식으로 저장하되, 2진법을 사용합니다. 64비트를 세 부분으로 나눕니다.

  1. 부호 비트 (1 비트): 0은 양수, 1은 음수.
  2. 지수 (11 비트): 2의 "거듭제곱"을 저장합니다. 이는 숫자의 범위(얼마나 크거나 작은지)를 결정합니다.
  3. 가수/유효숫자 (52 비트): 숫자의 정밀도 ( 1.000110011... 부분)를 저장합니다.

따라서 0.1은 다음과 같은 근사치로 저장됩니다.

+(1.100110011001... \times 2^{-4})


CPU가 부동 소수점을 계산하는 방법

CPU에 A + B를 계산하라고 요청하면, ALU(산술 논리 장치) 는 다음 단계를 수행합니다.

  1. 분해: CPU의 FPU(부동 소수점 장치)가 AB를 각각 부호, 지수, 가수의 세 부분으로 분해합니다.
  2. 지수 정렬: 두 숫자를 더하려면 "거듭제곱"(지수)이 같아야 합니다. CPU는 더 작은 지수를 가진 숫자의 가수를 큰 지수와 일치할 때까지 시프트(shift)합니다.
  3. 가수 덧셈: CPU의 가산기 회로가 두 가수 값을 더합니다.
  4. 재정규화: 결과를 다시 표준적인 부호/지수/가수 형식으로 변환하며, 이 과정에서 결과를 시프트하고 지수를 다시 조정할 수 있습니다.

유명한 부정확성: 0.1 + 0.2 != 0.3

이것이 부동 소수점 오류의 고전적인 예입니다.

  1. 컴퓨터는 0.1의 근사치 (0.1보다 약간 더 큰 값)를 저장합니다.
  2. 0.2의 근사치 (0.2보다 약간 더 작은 값)를 저장합니다.
  3. CPU가 이 두 부정확한 근사치를 더하면, 작은 오류들이 합쳐집니다.
  4. 그 결과는 컴퓨터가 0.3에 대해 저장하는 부정확한 근사치와 같지 않게 됩니다.

이것이 많은 프로그래밍 언어에서 0.1 + 0.20.30000000000000004와 같은 값으로 나오는 이유입니다.


돈 계산을 위한 해결책: 정확성이 중요할 때 💰

부동 소수점 숫자는 근사값이기 때문에 돈 계산에는 끔찍합니다. 금융 계산에 절대 사용해서는 안 됩니다.

대신 컴퓨터는 두 가지 다른 방법을 사용합니다.

  1. 고정 소수점 산술: 가장 일반적인 해결책입니다. 돈을 센트와 같은 가장 작은 단위를 나타내는 정수로 저장합니다.
    • $19.99를 저장하기 위해 정수 1999를 저장합니다.
    • $5.00를 저장하기 위해 500을 저장합니다.
    • $19.99 + $5.00 계산은 완벽하게 정확한 정수 덧셈 1999 + 500 = 2499가 됩니다.
    • 오직 화면에 표시할 때만 십진수(24.99)로 형식을 변환합니다.
  2. Decimal 데이터 타입: 일부 언어는 특별한 타입을 제공합니다 (자바의 BigDecimal이나 파이썬의 Decimal 등). 이러한 타입들은 2진 근사치를 사용하지 않습니다. 대신, 마치 사람이나 간단한 계산기가 하듯이, 숫자를 10진수 숫자 목록으로 저장하여 CPU의 고속 계산을 희생하는 대신 완벽한 10진수 정확도를 얻습니다.

1.238572983 + 4.29834759823674981 || pi + pi * 1.5

두 계산 모두 CPU의 FPU(부동 소수점 장치) 에 의해 처리되지만, 서로 다른 문제에 부딪히게 됩니다.


1. 정밀도의 문제: 1.238572983 + 4.29834759823674981

이 계산은 전적으로 정밀도에 관한 문제입니다.

이 계산을 정확하게 하려면?

이 계산을 올바르게 수행하려면, 표준 하드웨어 가속 부동 소수점 타입(float 또는 double)을 사용할 수 없습니다.

대신 자바의 BigDecimal이나 파이썬의 Decimal과 같은 특수 소프트웨어 기반 타입을 사용해야 합니다. 이러한 타입들은 숫자를 10진수 숫자 목록으로 저장하고, 완벽한 10진수 정밀도를 보장하기 위해 종이에서 계산하는 것처럼 더 느린 소프트웨어 기반의 "긴 수학(long math)"을 수행합니다.


2. 표현의 문제: pi + pi * 1.5

이 계산은 전적으로 표현에 관한 문제입니다.


요약: 속도 vs. 정확성


이 숫자들을 정확하게 계산하려면, CPU에 내장된 floatdouble 타입 대신 소프트웨어 기반의 Decimal 라이브러리를 사용해야 합니다.

이러한 라이브러리들은 2진법(binary)이 아닌, 마치 종이에 계산하듯 10진법(decimal)으로 수학을 수행하여 하드웨어의 한계를 피합니다.

가장 일반적인 예는 다음과 같습니다.


작동 원리

핵심적인 차이점은 다음과 같습니다.

매우 중요한 규칙: 완벽한 정확도를 얻으려면, 이 객체들을 반드시 문자열(string) 로 생성해야 합니다. 만약 double 타입에서 생성한다면, 부정확함이 이미 발생한 상태입니다.


예제

요청하신 계산을 정확하게 수행하는 방법은 다음과 같습니다.

1. 정밀도 문제: 1.238572983 + 4.29834759823674981

이 숫자는 18자리의 소수점을 가지며, 이는 표준 double이 담을 수 있는 것보다 많습니다.

Java (BigDecimal)

import java.math.BigDecimal;

// 완벽한 정밀도를 위해 문자열 생성자 사용
BigDecimal a = new BigDecimal("1.238572983");
BigDecimal b = new BigDecimal("4.29834759823674981");

BigDecimal result = a.add(b);

// 결과는 완벽하게 정확합니다.
System.out.println(result); 
// 출력: 5.53692058123674981

Python (Decimal)

from decimal import Decimal

# 문자열 생성자 사용
a = Decimal("1.238572983")
b = Decimal("4.29834759823674981")

result = a + b

# 결과는 완벽하게 정확합니다.
print(result)
# 출력: 5.53692058123674981

2. 표현의 문제: pi + pi * 1.5

$\pi$(파이)는 무리수이므로 절대 정확하게 표현될 수 없습니다. 하지만 double이 제공하는 것보다 훨씬 넘어서는, _필요한 만큼의 정밀도_로 계산할 수 있습니다.

먼저 원하는 "계산 문맥" 즉, 정밀도를 설정합니다.

Python (Decimal)

from decimal import Decimal, getcontext

# 원하는 정밀도를 100자리로 설정
getcontext().prec = 100

# 'pi'와 '1.5'를 고정밀도 Decimal로 로드
pi = Decimal('3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679')
one_point_five = Decimal('1.5')

result = pi + (pi * one_point_five)

print(result)
# 출력은 100자리까지 정확합니다:
# 7.85398163397448309615660845819875721049292349843776455243736148076954101571552249657008706335529267

트레이드오프: 속도 vs 정확성

우리가 모든 것에 BigDecimal을 사용하지 않는 간단한 이유가 있습니다: 속도입니다.

타입 계산 주체 속도 사용 사례
double CPU 하드웨어 (FPU) 매우 빠름 과학, 그래픽, 게임 (작은 오류가 중요하지 않은 경우)
BigDecimal 소프트웨어 매우 느림 금융, 돈, 결제 (100% 정확도가 필요한 경우)
references